from vcbrowser.engine.base import BaseEngine, EngineError
from vcbrowser.nodes import Node, FileNode, DirNode, NodeInfo
from vcbrowser.enumerations import NodeKind, RepositoryType
from mercurial import hg, ui
from mercurial.context import short
from mercurial.error import RepoError
from pprint import pformat

import time
import datetime
import logging
import os.path

class MercurialEngineError(EngineError):
    pass

class MercurialEngine(BaseEngine):
    """
    Backend for mercurial repositories browsing.
    For now only local repositories are supported.
    """
    
    HEAD_REVISION = -2

    def __init__(self, repo_url):
        super(MercurialEngine, self).__init__(repo_url)
        try:
            self.client = hg.repository(ui.ui(), repo_url)
        except RepoError, err:
            raise MercurialEngineError("Repository not found. "
                "Original error was: %s" % err)
        self._cached_changeset = {}
        self.revision_numbers = list(self.client)

    def _get_real_path(self, rel_url):
        """
        Returns real path to relative url.
        """
        return os.path.join(self.repo_url, rel_url)

    def _get_revision(self, revision):
        if revision is None:
            revision = MercurialEngine.HEAD_REVISION
        else:
            try:
                revision = int(revision)
            except ValueError, err:
                raise MercurialEngineError("Revision should be a number")
        if revision == -1:
            raise MercurialEngineError(
                "Cannot retrieve revision if its number is '-1'")
        elif revision < -1:
            revision = len(self.revision_numbers) + revision + 1
        if len(self.revision_numbers) == 0:
            raise MercurialEngineError("Repository has no changesets yet")
        if revision not in self.revision_numbers:
            raise MercurialEngineError("No revision %d found in the "
                "repository! Repositories available revisions: %d-%d."
                % (revision, self.revision_numbers[0],
                    self.revision_numbers[-1]))
        return revision

    def get_changeset(self, revision=HEAD_REVISION):
        revision = self._get_revision(revision)
        if not self._cached_changeset.has_key(revision):
            ctx = self.client[revision]
            self._cached_changeset[revision] = MercurialChangeset(ctx, revision)
        return self._cached_changeset[revision]

    def get_kind(self, url, revision=HEAD_REVISION):
        changeset = self.get_changeset(revision)
        return changeset.get_kind(url)

    def request(self, url, revision=HEAD_REVISION, fetch_content=True):
        """
        Returns node from the given url with selected rev.
        # TODO: should be reimplemented as it is much faster
        #       to walk using ctx object
        """
        revision = self._get_revision(revision)
        kind = self.get_kind(url, revision)
        if kind is NodeKind.FILE:
            if fetch_content:
                content = self.get_file_content(url, revision)
            else:
                content = None
            requested_node = FileNode(url, content)
            requested_node.info = self.get_changeset(revision).get_node_info(url)
        elif kind is NodeKind.DIR:
            nodes = self.fetch_nodes(url, revision)
            dirents = [node for node in nodes if node.is_dir() and not node.url=='']
            dirents.sort()
            filents = [node for node in nodes if node.is_file()]
            filents.sort()
            nodes = dirents + filents
            requested_node = DirNode(url, nodes)
            for node in nodes + [requested_node]:
                node.info = self.get_changeset(revision).get_node_info(node.url)
        else:
            raise MercurialEngineError("Kind undefined!")
        return requested_node

    def get_file_content(self, url, revision=HEAD_REVISION):
        if not self.get_kind(url, revision) is NodeKind.FILE:
            raise MercurialEngineError("Target is not a file!")
        changeset = self.get_changeset(revision)
        content = changeset.get_file_content(url)
        return content

    def fetch_nodes(self, url, revision=HEAD_REVISION):
        changeset = self.get_changeset(revision)
        return changeset.get_nodes(url)
 
    def walk(self, topurl, revision=HEAD_REVISION, fetch_files_content=False):
        """
        Similar to os.walk method. Insted of filesystem
        it walks through repository of the given topurl.
        Returns list of tuples (topnode, dirnodes, filenodes).
        """
        topnode = self.request(topurl, revision, fetch_content=fetch_files_content)
        if fetch_files_content:
            for filenode in topnode.files:
                filenode.content = self.get_file_content(filenode.url, revision)
        yield (topnode, topnode.dirs, topnode.files)
        for dirnode in topnode.dirs:
            for tup in self.walk(dirnode.url, revision, fetch_files_content):
                yield tup

class MercurialChangeset(object):
    """
    Container for changelog and list of files (and dirs as well).
    """
    def __init__(self, ctx, revno):
        """
        Retrieve information from ctx object (instance of
        mercurial.context.changectx).
        """
        self.revno = revno
        self.message = ctx.description()
        self.author = ctx.user()
        self.date = datetime.datetime.fromtimestamp(sum(ctx.date()))
        # Set files and dirs as nodes
        self.files = list(ctx)
        self.dirs = list(set(map(os.path.dirname, self.files)))
        self._ctx = ctx
    
    def __str__(self):
        return "%s:%s" % (self.revno, self._ctx)
    
    def _fixpath(self, url):
        if url in ('/', './', '.'):
            url = ''
        url = url.rstrip('/')
        return url

    def get_kind(self, url):
        url = self._fixpath(url)
        #url = url.lstrip('.').strip('/')
        if url in self.files:
            return NodeKind.FILE
        elif url in self.dirs:
            return NodeKind.DIR
        raise MercurialEngineError("There is no entry at url '%s' in revision %s"
            % (url, self))
    
    def get_file_content(self, url):
        if not self.get_kind(url) is NodeKind.FILE:
            raise MercurialEngineError("Targe is not a file!")
        url = self._fixpath(url)
        fctx = self._ctx[url]
        return fctx.data()

    def get_nodes(self, url):
        """
        Returns combined DirNode and FileNode objects list at the given url.
        """
        if not self.get_kind(url) is NodeKind.DIR:
            raise MercurialEngineError("Cannot retrieve list of nodes at the "
                "the given location %s as it is not a directory!" % url)
        url = self._fixpath(url)
        filenodes = [FileNode(f, content=None) for f in self.files if os.path.dirname(f) == url]
        dirs = set(map(os.path.dirname, self.files))
        dirnodes = [DirNode(d, nodes=[]) for d in dirs if os.path.dirname(d) == url]
        nodes = dirnodes + filenodes
        return nodes

    def get_node_info(self, url):
        """
        Returns requested node's information.
        """
        kind = self.get_kind(url)
        if kind is NodeKind.DIR:
            info = NodeInfo(
                size = None,
                revision = self.revno,
                date = datetime.datetime.fromtimestamp(sum(self._ctx.date())), 
                author = self._ctx.user(),
                message = self._ctx.description(),
            )
        elif kind is NodeKind.FILE:
            fctx = self._ctx.filectx(url)
            info = NodeInfo(
                size = fctx.size(),
                revision = self.revno,
                date = datetime.datetime.fromtimestamp(sum(fctx.date())),
                author = fctx.user(),
                message = fctx.description(),
            )
        else:
            raise MercurialEngineError("Info cannot be retrieved as there is "
                "no file nor dir node at requested url: %s" % url)
        return info

    
